package mp3player;

import java.io.IOException;
import java.io.InputStream;

import javax.sound.sampled.FloatControl;

import javazoom.jl.decoder.JavaLayerException;
import javazoom.jl.player.JavaSoundAudioDevice;
import javazoom.jl.player.advanced.AdvancedPlayer;
import javazoom.jl.player.advanced.PlaybackEvent;
import javazoom.jl.player.advanced.PlaybackListener;

/**
 * A class for playing MP3s.
 */
public class Engine extends PlaybackListener implements Runnable {
	private class CountingPlayer extends AdvancedPlayer {
		private int frameCount = 0;
		private int volume = 70;
		private VolumeControlledAudioDevice dev;
		
		public CountingPlayer(InputStream stream, VolumeControlledAudioDevice dev) throws JavaLayerException {
			super(stream, dev);
			this.dev = dev;
		}
		
		protected boolean decodeFrame() throws JavaLayerException {
			if (frameCount == 1) {
				dev.setVolume(volume);
				if (Engine.this.listener != null)
					Engine.this.listener.updateSongStarted();
			}
			
			if (Engine.this.listener != null && (frameCount & 0x7f) == 0) {
				float pos = Engine.this.getPosition();
				Engine.this.listener.updateSongPosition(pos);
			}
			
			frameCount++;
			
			return super.decodeFrame();
		}
		
		public int getFrameCount() {
			return frameCount;
		}
		
		public void setVolume(int vol) {
			this.volume = vol;
		}
	}
	
	private static class VolumeControlledAudioDevice
	extends JavaSoundAudioDevice {
		public void setVolume(int vol) {
			if (source == null)
				return;
			
			// Get the gain control.
			
			if (source.isControlSupported(FloatControl.Type.VOLUME)) {
				FloatControl volc = (FloatControl)source.getControl(FloatControl.Type.VOLUME);
				volc.setValue(
						volc.getMinimum() + ((float)vol / 100.0f) *
						(volc.getMaximum() - volc.getMinimum()));
			} else if (source.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
				FloatControl gc = (FloatControl)source.getControl(FloatControl.Type.MASTER_GAIN);
				
				float min = gc.getMinimum();
				if (min < -25.0f) min = -25.0f;
				float max = gc.getMaximum();
				if (max > 10.0f) max = 10.0f;
				
				gc.setValue(min + (float)vol/100.0f * (max-min));
			}
		}
	}
	
	private enum State {
		STOPPED, PLAYING;
	}
	
	private MP3 mp3;
	
	private State state;
	private int frame;
	private final int lastFrame;
	
	private VolumeControlledAudioDevice dev;
	private CountingPlayer player;
	private Thread playerThread;
	private int playerStartFrame;
	int currentVolume = 70;
	
	private boolean requestedStop = false;
	
	private EngineListener listener;
	
	/**
	 * Creates a new MP3 player.
	 * @param mp3 File to play.
	 */
	public Engine(MP3 mp3, EngineListener listener) {
		this.mp3 = mp3;
		this.listener = listener;
		
		state = State.STOPPED;
		frame = 0;
		lastFrame = mp3.getFrames()-1;
	}
	
	/**
	 * The play function.
	 */
	public void play() throws JavaLayerException, IOException {
		if (state == State.PLAYING)
			return;
		
		requestedStop = false;
		
		InputStream mp3in = mp3.getStream();
		dev = new VolumeControlledAudioDevice();
		player = new CountingPlayer(mp3in, dev);
		player.setPlayBackListener(this);
		player.setVolume(currentVolume);
		
		playerStartFrame = frame;
		
		playerThread = new Thread(this);
		playerThread.start();
		
		state = State.PLAYING;
	}
	
	/**
	 * The pause function.
	 */
	public void pause() {
		if (state == State.STOPPED)
			return;
		
		requestedStop = true;
		player.stop();
		
		state = State.STOPPED;
	}
	
	/**
	 * The stop function.
	 */
	public void stop() {
		pause();
		frame = 0;
		if (listener != null)
			listener.updateSongPosition(0.0f);
	}
	
	/**
	 * Sets the playback volume.
	 * @param vol Volume in the range 0 - 100.
	 */
	public void setVolume(int vol) {
		currentVolume = vol;
		if (state == State.PLAYING) {
			dev.setVolume(vol);
			player.setVolume(vol);
		}
	}
	
	/**
	 * Seeks to the specified frame.
	 * @param frame Frame to seek to.
	 */
	public void seekToFrame(int frame) throws JavaLayerException, IOException {
		if (!mp3.isSeekable())
			return;
		
		boolean wasPlaying = false;
		if (state == State.PLAYING) {
			pause();
			wasPlaying = true;
		}
		
		if (frame < 0)
			frame = 0;
		else if (frame > lastFrame)
			frame = lastFrame+1;
		
		this.frame = frame;
		
		if (wasPlaying)
			play();
	}
	
	/**
	 * Seeks to the frame closest to the specified time.
	 * @param ms Time to seek to, in ms.
	 */
	public void seekToTime(float ms) throws JavaLayerException, IOException {
		seekToFrame(mp3.getClosestFrame(ms));
	}
	
	/**
	 * Seeks to the frame closest to the specified position.
	 * @param percent Position to seek to, 0 - 100.
	 */
	public void seek(float percent) throws JavaLayerException, IOException {
		seekToTime(mp3.getLength() * percent / 100.0f);
	}
	
	/**
	 * Get position in file in frames.
	 * @return Playback position in frames.
	 */
	public int getPositionFrame() {
		int frame = playerStartFrame;
		if (player != null)
			frame += player.getFrameCount();
		return frame;
	}
	
	/**
	 * Get position in milliseconds.
	 * @return Playback position in milliseconds.
	 */
	public float getPositionTime() {
		int frame = getPositionFrame();
		int len = lastFrame+1;
		if (len > 0)
			return (float)frame/len * mp3.getLength();
		else
			return 0.0f;
	}
	
	/**
	 * Returns the current position in the file, in percent.
	 * @return Position in percent, 0 - 100.
	 */
	public float getPosition() {
		float pos = getPositionTime();
		float length = mp3.getLength();
		if (length > 0.0f)
			return pos/length * 100;
		else
			return 0.0f;
	}

	/**
	 * PlaybackListener implementation.
	 */
	@Override
	public void playbackFinished(PlaybackEvent event) {
		/*
		 * event.getFrame() returns number of milliseconds
		 * played after the .play() command, not the frame
		 * where the playback stopped.
		 */
		frame = playerStartFrame + mp3.getClosestFrame(event.getFrame());
		state = State.STOPPED;
		
		if (frame >= lastFrame-10)
			frame = 0;
		
		if (!requestedStop && listener != null) {
			listener.updateSongEnded();
		}
	}

	/**
	 * PlaybackListener implementation.
	 */
	@Override
	public void playbackStarted(PlaybackEvent event) {
		dev.setVolume(currentVolume);
		player.setVolume(currentVolume);
	}

	/**
	 * Runnable implementation.
	 * Code for the player thread.
	 */
	public void run() {
		try {
			if (mp3.isSeekable()) {
				player.play(frame, lastFrame);
			} else {
				player.play();
			}
		} catch (JavaLayerException e) {
			// Error message?
			e.printStackTrace();
			state = State.STOPPED;
		}
	}
}
